/**
 * 20220114:created by jordia
 * 20220208：use [vr-button],the copy of [VRButton]
 */
import {EventDispatcher,Vector3,Matrix4,Color,Line,LineBasicMaterial,Mesh,MeshBasicMaterial,RingGeometry,BufferGeometry,Object3D} from "three";
import {XRControllerModelFactory} from "three/examples/jsm/webxr/XRControllerModelFactory";
import {VRButton} from "./vr-button";
import {ARButton} from "three/examples/jsm/webxr/ARButton";
import XRConstants from "./xr-constants";

const COMPARE_16=(a,b,precision)=>{
    for(let i=0;i<16;i++){
        if(Math.abs(a[i]-b[i])>precision)return false;
    }
    return true;
};

/**
 * XR initialization
 */
export default class XRBase {
    constructor(renderer,containerDiv,xrType=XRConstants.XR_TYPE_VR){
        this._renderer=renderer;
        this._container=containerDiv;
        this._xrType=xrType;
        this._init();
    }

    //.....................................................public....................................................

    /**
     * XR button
     * @returns {null|HTMLButtonElement|HTMLAnchorElement}
     */
    get button(){
        return this._xrBtn;
    }

    /**
     * xr manager
     * @returns {WebXRManager}
     */
    get manager(){
        return this._renderer.xr;
    }

    /**
     * xr session
     * @returns {*}
     */
    get session(){
        return this._renderer.xr.getSession();
    }

    /**
     * （almost）original event center
     * @returns {EventDispatcher}
     */
    get eventCenter(){
        return this._eventCenter;
    }

    /**
     * keep sustain-event dispatching（cause [move] event never stop）
     * @param bol
     */
    set autoUpdate(bol){
        this._autoUpdate=!!bol;
    }

    /**
     * all controllers(add them to the scene...)
     * @returns {*[]}
     */
    get controllers(){
        return [...this._rays,...this._hands,...this._grips];
    }

    /**
     * target ray controller(generally,the real controller)
     * @returns {[]|Array}
     */
    get rays(){
        return this._rays;
    }

    /**
     * grips model
     * @returns {[]|Array}
     */
    get grips(){
        return this._grips;
    }

    /**
     * hand controller(not sure yet,needs to be implemented)
     * @returns {[]|Array}
     */
    get hands(){
        return this._hands;
    }

    /**
     * threshold for matrix-checking
     * @param num
     */
    set threshold(num){
        this._changeThreshold=Math.max(num,0);
    }

    /**
     * estimated controller count(should never less than the real count)
     * @param num
     */
    set maxControllerCount(num){
        this._estimateCtrCount=Math.max(num,0);
    }

    /**
     * device profile
     * @returns {*}
     */
    get deviceProfile(){
        return this._deviceProfile;
    }

    /**
     * controller profile
     * @returns {*}
     */
    get controllerProfile(){
        return this._controllerProfile;
    }

    /**
     * generate move and moveChange event(if ._autoUpdate=true)
     * @param ctr
     */
    updateRay(ctr){
        this._moveEvent.paras.target=ctr;
        this._eventCenter.dispatchEvent(this._moveEvent);
        let pm=ctr.userData.hasOwnProperty('previousMatrixWorld')?ctr.userData['previousMatrixWorld']:new Matrix4();
        if(COMPARE_16(pm.elements,ctr.matrixWorld.elements,this._changeThreshold))return;
        this._moveChangeEvent.paras.target=ctr;
        this._eventCenter.dispatchEvent(this._moveChangeEvent);
        ctr.userData['previousMatrixWorld']=pm.copy(ctr.matrixWorld);
    }

    dispose(){
        for(let i=0;i<this._rays.length;i++){
            this._destroyController(this._rays[i]);
            this._destroyController(this._grips[i]);
            this._destroyController(this._hands[i]);
        }
        this._rays.length=0;
        this._grips.length=0;
        this._hands.length=0;
        if(this._xrBtn)this._container.removeChild(this._xrBtn);
        this._renderer.xr.removeEventListener(XRConstants.MANAGER_EVENT_SESSIONSTART,this._onSessionStart);
        this._renderer.xr.removeEventListener(XRConstants.MANAGER_EVENT_SESSIONEND,this._onSessionEnd);
        this._renderer.xr.enabled=false;
        this._renderer=null;
    }

    //.....................................................private....................................................

    _destroyController(ctr){
        ctr.removeEventListener(XRConstants.CONTROLLER_EVENT_COMMON_SELECT,this._onSelect);
        ctr.removeEventListener(XRConstants.CONTROLLER_EVENT_COMMON_SELECTSTART,this._onSelectStart);
        ctr.removeEventListener(XRConstants.CONTROLLER_EVENT_COMMON_SELECTEND,this._onSelectEnd);
        ctr.removeEventListener(XRConstants.CONTROLLER_EVENT_COMMON_SQUEEZE,this._onSqueeze);
        ctr.removeEventListener(XRConstants.CONTROLLER_EVENT_COMMON_SQUEEZESTART,this._onSqueezeStart);
        ctr.removeEventListener(XRConstants.CONTROLLER_EVENT_COMMON_SQUEEZEEND,this._onSqueezeEnd);
        ctr.removeEventListener(XRConstants.CONTROLLER_EVENT_COMMON_CONNECTED,this._onConnected);
        ctr.removeEventListener(XRConstants.CONTROLLER_EVENT_COMMON_DISCONNECTED,this._onDisconnected);
        ctr.removeEventListener(XRConstants.CONTROLLER_EVENT_RAY_MOVE,this._onMove);
        ctr.removeEventListener(XRConstants.CONTROLLER_EVENT_HAND_PINCHSTART,this._onPinchStart);
        ctr.removeEventListener(XRConstants.CONTROLLER_EVENT_HAND_PINCHEND,this._onPinchEnd);
        if(ctr.parent)ctr.parent.remove(ctr);
        ctr.traverse((c)=>{
            if(c.geometry)c.geometry.dispose();
            if(c.material)c.material.dispose();
        });
    }
    _buildControllerObject(targetRayMode){
        let g,m;
        switch (targetRayMode) {
            case XRConstants.TARGET_RAY_MODE_TRACKED_POINTER:
                g=new BufferGeometry().setFromPoints([new Vector3(0,0,0),new Vector3(0,0,-1)]);
                m=new LineBasicMaterial({color:new Color('#ff00ff')});
                return new Line(g,m);
            case XRConstants.TARGET_RAY_MODE_GAZE:
                g=new RingGeometry(0.02,0.04,32).translate(0,0,-1);
                m=new MeshBasicMaterial({color:new Color('#ff00ff')});
                return new Mesh(g,m);
            case XRConstants.TARGET_RAY_MODE_SCREEN:
                console.warn('targetRayMode [screen] needs to be implemented!');
                return new Object3D();
        }
    }

    _init(){
        this._initVariable();
        this._initComponents();
        this._initEventBind();
    }
    _initVariable(){
        this._rays=[];
        this._grips=[];
        this._hands=[];
        this._autoUpdate=true;
        this._eventCenter=new EventDispatcher();
        this._xrBtn=null;
        this._modelFactory=new XRControllerModelFactory();
        this._rayLen=1000;
        this._inputSources=[];
        this._changeThreshold=0.005;
        this._estimateCtrCount=8;
        this._deviceProfile=null;
        this._controllerProfile=null;

        this._moveEvent={type:XRConstants.CONTROLLER_EVENT_RAY_MOVE,paras:{}};
        this._moveChangeEvent={type:XRConstants.CONTROLLER_EVENT_RAY_MOVECHANGE,paras:{}};
        this._pinchStartEvent={type:XRConstants.CONTROLLER_EVENT_HAND_PINCHSTART,paras:{}};
        this._pinchEndEvent={type:XRConstants.CONTROLLER_EVENT_HAND_PINCHEND,paras:{}};
        this._connectedEvent={type:XRConstants.CONTROLLER_EVENT_COMMON_CONNECTED,paras:{}};
        this._disconnectedEvent={type:XRConstants.CONTROLLER_EVENT_COMMON_DISCONNECTED,paras:{}};
        this._selectEvent={type:XRConstants.CONTROLLER_EVENT_COMMON_SELECT,paras:{}};
        this._selectStartEvent={type:XRConstants.CONTROLLER_EVENT_COMMON_SELECTSTART,paras:{}};
        this._selectEndEvent={type:XRConstants.CONTROLLER_EVENT_COMMON_SELECTEND,paras:{}};
        this._squeezeEvent={type:XRConstants.CONTROLLER_EVENT_COMMON_SQUEEZE,paras:{}};
        this._squeezeStartEvent={type:XRConstants.CONTROLLER_EVENT_COMMON_SQUEEZESTART,paras:{}};
        this._squeezeEndEvent={type:XRConstants.CONTROLLER_EVENT_COMMON_SQUEEZEEND,paras:{}};
        this._sessionStartEvent={type:XRConstants.MANAGER_EVENT_SESSIONSTART,paras:{}};
        this._sessionEndEvent={type:XRConstants.MANAGER_EVENT_SESSIONEND,paras:{}};
        this._inputSourcesChangeEvent={type:XRConstants.SESSION_EVENT_INPUTSOURCESCHANGE,paras:{}};
    }
    _initComponents(){
        this._renderer.xr.enabled=true;
        switch (this._xrType) {
            case XRConstants.XR_TYPE_VR:
                this._xrBtn=VRButton.createButton(this._renderer);
                break;
            case XRConstants.XR_TYPE_AR:
                console.warn('[AR] needs to be implemented!');
                this._xrBtn=ARButton.createButton(this._renderer);
                break;
        }
        this._container.appendChild(this._xrBtn);
    }
    _initEventBind(){
        this._onInputSourcesChange=(e)=>{
            /**
             * todo:
             *      confirm: possible conflict with [onConnect/onDisconnect]
             */
            this._inputSources = e.added;
            if(this._inputSources.length){
                let profiles=this._inputSources[0]['profiles'];
                if(profiles&&profiles.length){
                    this._deviceProfile=profiles[0];
                    this._controllerProfile=profiles[1];
                }
            }
            for (let i = 0; i < this._estimateCtrCount; i++) {
                if(this._inputSources.length>i){
                    if(this._rays[i])continue;
                    this._initRay(i);
                    this._initGrip(i);
                    this._initHand(i);
                    let iso=this._inputSources[i];
                    this._rays[i].dispatchEvent({type:XRConstants.CONTROLLER_EVENT_COMMON_CONNECTED,data:iso});
                    this._grips[i].dispatchEvent({type:XRConstants.CONTROLLER_EVENT_COMMON_CONNECTED,data:iso});
                    this._hands[i].dispatchEvent({type:XRConstants.CONTROLLER_EVENT_COMMON_CONNECTED,data:iso});
                }else{
                    this._destroyController(this._rays[i]);
                    this._destroyController(this._grips[i]);
                    this._destroyController(this._hands[i]);
                }
            }
            for(let i=0;i<this._inputSources.length;i++){
                this._rays[i].userData.inputSource=this._inputSources[i];
            }
            this._rays.length=this._grips.length=this._hands.length=this._inputSources.length;
            this._inputSourcesChangeEvent.paras.target = this;
            this._inputSourcesChangeEvent.paras.data = e;
            this._eventCenter.dispatchEvent(this._inputSourcesChangeEvent);
        };
        this._onSessionStart=(e)=> {
            for(let i=0;i<this._estimateCtrCount;i++){
                this._initRay(i);
                this._initGrip(i);
                this._initHand(i);
            }
            this._sessionStartEvent.paras.target = this;
            this._sessionStartEvent.paras.data = e;
            this._eventCenter.dispatchEvent(this._sessionStartEvent);
            this._session=this.session;
            this._session.addEventListener(XRConstants.SESSION_EVENT_INPUTSOURCESCHANGE,this._onInputSourcesChange);
        };
        this._onSessionEnd=(event)=>{
            for(let i=0;i<this._rays.length;i++){
                this._destroyController(this._rays[i]);
                this._destroyController(this._grips[i]);
                this._destroyController(this._hands[i]);
            }
            this._rays.length=this._grips.length=this._hands.length=0;
            this._sessionEndEvent.paras.target=this;
            this._sessionEndEvent.paras.data=event;
            this._eventCenter.dispatchEvent(this._sessionEndEvent);
            this._session.removeEventListener(XRConstants.SESSION_EVENT_INPUTSOURCESCHANGE,this._onInputSourcesChange);
        };
        this._onDisconnected=(event)=>{
            let ctr=event.target;
            if(ctr.userData.model){
                let md=ctr.userData.model;
                ctr.remove(md);
                md.geometry.dispose();
                md.material.dispose();
                ctr.userData.model=undefined;
            }
            this._dispatchCommonEvent(this._disconnectedEvent,event);
        };
        this._onConnected=(event)=>{
            let targetRayMode=event.data['targetRayMode'];
            let md=this._buildControllerObject(targetRayMode);
            md.scale.multiplyScalar(this._rayLen);
            let ctr=event.target;
            ctr.add(md);
            ctr.userData.model=md;
            ctr.userData.inputSource=event.data;
            this._dispatchCommonEvent(this._connectedEvent,event);
        };
        this._onSelect=(event)=>{
            this._dispatchCommonEvent(this._selectEvent,event);
        };
        this._onSelectStart=(event)=>{
            this._dispatchCommonEvent(this._selectStartEvent,event);
        };
        this._onSelectEnd=(event)=>{
            this._dispatchCommonEvent(this._selectEndEvent,event);
        };
        this._onSqueeze=(event)=>{
            this._dispatchCommonEvent(this._squeezeEvent,event);
        };
        this._onSqueezeStart=(event)=>{
            this._dispatchCommonEvent(this._squeezeStartEvent,event);
        };
        this._onSqueezeEnd=(event)=>{
            this._dispatchCommonEvent(this._squeezeEndEvent,event);
        };
        this._onMove=(event)=>{
            if(this._autoUpdate)this.updateRay(event.target);
        };
        this._onPinchStart=(event)=>{
            this._pinchStartEvent.paras.handedness=event.handedness;
            this._pinchStartEvent.paras.target=event.target;
            this._eventCenter.dispatchEvent(this._pinchStartEvent);
        };
        this._onPinchEnd=(event)=>{
            this._pinchEndEvent.paras.handedness=event.handedness;
            this._pinchEndEvent.paras.target=event.target;
            this._eventCenter.dispatchEvent(this._pinchEndEvent);
        };

        this.manager.addEventListener(XRConstants.MANAGER_EVENT_SESSIONSTART,this._onSessionStart);
        this.manager.addEventListener(XRConstants.MANAGER_EVENT_SESSIONEND,this._onSessionEnd);
    }
    _initRay(index){
        let ctr=this.manager.getController(index);
        ctr.addEventListener(XRConstants.CONTROLLER_EVENT_COMMON_SELECT,this._onSelect);
        ctr.addEventListener(XRConstants.CONTROLLER_EVENT_COMMON_SELECTSTART,this._onSelectStart);
        ctr.addEventListener(XRConstants.CONTROLLER_EVENT_COMMON_SELECTEND,this._onSelectEnd);
        ctr.addEventListener(XRConstants.CONTROLLER_EVENT_COMMON_SQUEEZE,this._onSqueeze);
        ctr.addEventListener(XRConstants.CONTROLLER_EVENT_COMMON_SQUEEZESTART,this._onSqueezeStart);
        ctr.addEventListener(XRConstants.CONTROLLER_EVENT_COMMON_SQUEEZEEND,this._onSqueezeEnd);
        ctr.addEventListener(XRConstants.CONTROLLER_EVENT_COMMON_CONNECTED,this._onConnected);
        ctr.addEventListener(XRConstants.CONTROLLER_EVENT_COMMON_DISCONNECTED,this._onDisconnected);
        ctr.addEventListener(XRConstants.CONTROLLER_EVENT_RAY_MOVE,this._onMove);
        this._rays.push(ctr);
    }
    _initGrip(index){
        let gp=this.manager.getControllerGrip(index);
        let model=this._modelFactory.createControllerModel(gp);
        gp.add(model);
        gp.userData.model=model;
        this._grips.push(gp);
    }
    _initHand(index){
        let hd=this.manager.getHand(index);
        /**
         * model
         */
        hd.addEventListener(XRConstants.CONTROLLER_EVENT_HAND_PINCHSTART,this._onPinchStart);
        hd.addEventListener(XRConstants.CONTROLLER_EVENT_HAND_PINCHEND,this._onPinchEnd);
        this._hands.push(hd);
    }
    _dispatchCommonEvent(event,info){
        event.paras.target=info.target;
        event.paras.data=info.data;
        this._eventCenter.dispatchEvent(event);
    }
}